package org.balthie.demo.sum.misc.direct;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSigner;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.jar.Manifest;
import sun.misc.Resource;
import sun.misc.URLClassPath;

public class JarFileClassLoader extends URLClassLoader
{
    private final URLClassPath ucp;
    
    private final String longName = "自定义classloader";
    
    private final String shortName = "jfcl";
    
    public JarFileClassLoader(URL[] urls)
    {
        super(urls, JarFileClassLoader.getSystemClassLoader());
        this.ucp = new URLClassPath(urls);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        System.out.println("自定义 findClass " + name);
//        return super.findClass(name);
        String path = name.replace('.', '/').concat(".class");
        Resource res = ucp.getResource(path, false);
        if(res != null)
        {
            try
            {
                return this.defineClass(name, res);
            }
            catch (Exception e)
            {
                throw new ClassNotFoundException(name);
            }
        }
        else
        {
            return null;
        }
    }
    
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException
    {
        System.out.println("自定义 loadClass" + name);
        return this.findClass(name);
    }
    
    private Class<?> defineClass(String name, Resource res) throws Exception
    {
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if(i != -1)
        {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
//            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if(bb != null)
        {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            return defineClass(name, bb, cs);
        }
        else
        {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            
            ProtectionDomain protectionDomain = this.preDefineClass(name, null);
            String source = defineClassSourceLocation(protectionDomain);
            
            try
            {
//                String name, byte[] b, int off, int len,
//                ProtectionDomain protectionDomain
                Method[] methods = this.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethods();
                
                Method method = Arrays.asList(methods).stream().filter(m -> m.getName().equalsIgnoreCase("defineClass1"))
                        .findFirst().get();
                method.setAccessible(true);
                method.invoke(this, name, b, 0, b.length, protectionDomain, source);
            }
            catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
                throw e;
            }
            
            return super.defineClass(name, b, 0, b.length, new ProtectionDomain(cs, null, this, null));
        }
    }
    
    private ProtectionDomain preDefineClass(String name,
            ProtectionDomain pd)
    {
        if((name != null) && name.startsWith("javaxxxxx."))
        {
            throw new SecurityException("Prohibited package name: "
                    +
                    name.substring(0, name.lastIndexOf('.')));
        }
        if(pd == null)
        {
            pd = new ProtectionDomain(new CodeSource(null, (java.security.cert.Certificate[]) null), null, this, null);
        }
        
        return pd;
    }
    
    private void definePackageInternal(String pkgname, Manifest man, URL url)
    {
        if(man != null)
        {
            definePackage(pkgname, man, url);
        }
        else
        {
            definePackage(pkgname, null, null, null, null, null, null, null);
        }
    }
    
    private String defineClassSourceLocation(ProtectionDomain pd)
    {
        CodeSource cs = pd.getCodeSource();
        String source = null;
        if(cs != null && cs.getLocation() != null)
        {
            source = cs.getLocation().toString();
        }
        return source;
    }
}
